import { BinaryWriter } from '../io/binarywriter.js';
import { Utf8StringToArrayBuffer } from '../io/bufferutils.js';
import { FileFormat, GetFileExtension, GetFileName } from '../io/fileutils.js';
import { MeshInstanceId } from '../model/meshinstance.js';
import { RGBColor, SRGBToLinear } from '../model/color.js';
import { MaterialType } from '../model/material.js';
import { ConvertMeshToMeshBuffer } from '../model/meshbuffer.js';
import { ExportedFile, ExporterBase } from './exporterbase.js';

const GltfComponentType =
{
    UNSIGNED_INT : 5125,
    FLOAT : 5126
};

const GltfBufferType =
{
    ARRAY_BUFFER : 34962,
    ELEMENT_ARRAY_BUFFER : 34963
};

export class ExporterGltf extends ExporterBase
{
	constructor ()
	{
		super ();
        this.components = {
            index : {
                type : GltfComponentType.UNSIGNED_INT,
                size : 4
            },
            number : {
                type : GltfComponentType.FLOAT,
                size : 4
            }
        };
	}

    CanExport (format, extension)
    {
        return (format === FileFormat.Text && extension === 'gltf') || (format === FileFormat.Binary && extension === 'glb');
    }

	ExportContent (exporterModel, format, files, onFinish)
	{
        if (format === FileFormat.Text) {
            this.ExportAsciiContent (exporterModel, files);
        } else if (format === FileFormat.Binary) {
            this.ExportBinaryContent (exporterModel, files);
        }
        onFinish ();
	}

	ExportAsciiContent (exporterModel, files)
	{
        let gltfFile = new ExportedFile ('model.gltf');
        let binFile = new ExportedFile ('model.bin');
        files.push (gltfFile);
        files.push (binFile);

        let meshDataArr = this.GetMeshData (exporterModel);
        let mainBuffer = this.GetMainBuffer (meshDataArr);
        let mainJson = this.GetMainJson (exporterModel, meshDataArr);
        mainJson.buffers.push ({
            uri : binFile.GetName (),
            byteLength : mainBuffer.byteLength
        });

        let fileNameToIndex = new Map ();
        this.ExportMaterials (exporterModel, mainJson, (texture) => {
            let fileName = GetFileName (texture.name);
            if (fileNameToIndex.has (fileName)) {
                return fileNameToIndex.get (fileName);
            } else {
                let textureFile = new ExportedFile (fileName);
                textureFile.SetBufferContent (texture.buffer);
                files.push (textureFile);

                let textureIndex = mainJson.textures.length;
                fileNameToIndex.set (fileName, textureIndex);

                mainJson.images.push ({
                    uri : fileName
                });

                mainJson.textures.push ({
                    source : textureIndex
                });

                return textureIndex;
            }
        });

        gltfFile.SetTextContent (JSON.stringify (mainJson, null, 4));
        binFile.SetBufferContent (mainBuffer);
    }

    ExportBinaryContent (exporterModel, files)
    {
        function AlignToBoundary (size)
        {
            let remainder = size % 4;
            if (remainder === 0) {
                return size;
            }
            return size + (4 - remainder);
        }

        function WriteCharacters (writer, char, count)
        {
            for (let i = 0; i < count; i++) {
                writer.WriteUnsignedCharacter8 (char);
            }
        }

        let glbFile = new ExportedFile ('model.glb');
        files.push (glbFile);

        let meshDataArr = this.GetMeshData (exporterModel);
        let mainBuffer = this.GetMainBuffer (meshDataArr);
        let mainJson = this.GetMainJson (exporterModel, meshDataArr);

        let textureBuffers = [];
        let textureOffset = mainBuffer.byteLength;

        let fileNameToIndex = new Map ();
        this.ExportMaterials (exporterModel, mainJson, (texture) => {
            let fileName = GetFileName (texture.name);
            let extension = GetFileExtension (texture.name);
            if (fileNameToIndex.has (fileName)) {
                return fileNameToIndex.get (fileName);
            } else {
                let bufferViewIndex = mainJson.bufferViews.length;
                let textureIndex = mainJson.textures.length;
                fileNameToIndex.set (fileName, textureIndex);
                let textureBuffer = texture.buffer;
                textureBuffers.push (textureBuffer);
                mainJson.bufferViews.push ({
                    buffer : 0,
                    byteOffset : textureOffset,
                    byteLength : textureBuffer.byteLength
                });
                textureOffset += textureBuffer.byteLength;
                mainJson.images.push ({
                    bufferView : bufferViewIndex,
                    mimeType : 'image/' + extension
                });
                mainJson.textures.push ({
                    source : textureIndex
                });

                return textureIndex;
            }
        });

        let mainBinaryBufferLength = mainBuffer.byteLength;
        for (let i = 0; i < textureBuffers.length; i++) {
            let textureBuffer = textureBuffers[i];
            mainBinaryBufferLength += textureBuffer.byteLength;
        }
        let mainBinaryBufferAlignedLength = AlignToBoundary (mainBinaryBufferLength);
        mainJson.buffers.push ({
            byteLength : mainBinaryBufferAlignedLength
        });

        let mainJsonString = JSON.stringify (mainJson);
        let mainJsonBuffer = Utf8StringToArrayBuffer (mainJsonString);
        let mainJsonBufferLength = mainJsonBuffer.byteLength;
        let mainJsonBufferAlignedLength = AlignToBoundary (mainJsonBufferLength);

        let glbSize = 12 + 8 + mainJsonBufferAlignedLength + 8 + mainBinaryBufferAlignedLength;
        let glbWriter = new BinaryWriter (glbSize, true);

        glbWriter.WriteUnsignedInteger32 (0x46546C67);
        glbWriter.WriteUnsignedInteger32 (2);
        glbWriter.WriteUnsignedInteger32 (glbSize);

        glbWriter.WriteUnsignedInteger32 (mainJsonBufferAlignedLength);
        glbWriter.WriteUnsignedInteger32 (0x4E4F534A);
        glbWriter.WriteArrayBuffer (mainJsonBuffer);
        WriteCharacters (glbWriter, 32, mainJsonBufferAlignedLength - mainJsonBufferLength);

        glbWriter.WriteUnsignedInteger32 (mainBinaryBufferAlignedLength);
        glbWriter.WriteUnsignedInteger32 (0x004E4942);
        glbWriter.WriteArrayBuffer (mainBuffer);

        for (let i = 0; i < textureBuffers.length; i++) {
            let textureBuffer = textureBuffers[i];
            glbWriter.WriteArrayBuffer (textureBuffer);
        }
        WriteCharacters (glbWriter, 0, mainBinaryBufferAlignedLength - mainBinaryBufferLength);

        glbFile.SetBufferContent (glbWriter.GetBuffer ());
    }

    GetMeshData (exporterModel)
    {
        let meshDataArr = [];

        exporterModel.EnumerateMeshes ((mesh) => {
            let buffer = ConvertMeshToMeshBuffer (mesh);
            meshDataArr.push ({
                name : mesh.GetName (),
                buffer : buffer,
                offsets : [],
                sizes : []
            });
        });

        return meshDataArr;
    }

    GetMainBuffer (meshDataArr)
    {
        let mainBufferSize = 0;
        for (let meshData of meshDataArr) {
            mainBufferSize += meshData.buffer.GetByteLength (this.components.index.size, this.components.number.size);
        }

        let writer = new BinaryWriter (mainBufferSize, true);
        for (let meshData of meshDataArr) {
            for (let primitiveIndex = 0; primitiveIndex < meshData.buffer.PrimitiveCount (); primitiveIndex++) {
                let primitive = meshData.buffer.GetPrimitive (primitiveIndex);
                let offset = writer.GetPosition ();
                for (let i = 0; i < primitive.indices.length; i++) {
                    writer.WriteUnsignedInteger32 (primitive.indices[i]);
                }
                for (let i = 0; i < primitive.vertices.length; i++) {
                    writer.WriteFloat32 (primitive.vertices[i]);
                }
                for (let i = 0; i < primitive.colors.length; i++) {
                    writer.WriteFloat32 (SRGBToLinear (primitive.colors[i]));
                }
                for (let i = 0; i < primitive.normals.length; i++) {
                    writer.WriteFloat32 (primitive.normals[i]);
                }
                for (let i = 0; i < primitive.uvs.length; i++) {
                    let texCoord = primitive.uvs[i];
                    if (i % 2 === 1) {
                        texCoord *= -1.0;
                    }
                    writer.WriteFloat32 (texCoord);
                }
                meshData.offsets.push (offset);
                meshData.sizes.push (writer.GetPosition () - offset);
            }
        }

        return writer.GetBuffer ();
    }

    GetMainJson (exporterModel, meshDataArr)
    {
        class BufferViewCreator
        {
            constructor (mainJson, byteOffset)
            {
                this.mainJson = mainJson;
                this.byteOffset = byteOffset;
            }

            AddBufferView (byteLength, target)
            {
                let bufferView = {
                    buffer : 0,
                    byteOffset : this.byteOffset,
                    byteLength : byteLength,
                    target : target
                };
                this.mainJson.bufferViews.push (bufferView);
                this.byteOffset += byteLength;
                return this.mainJson.bufferViews.length - 1;
            }
        }

        function NodeHasVisibleChildren (model, node)
        {
            for (let meshIndex of node.GetMeshIndices ()) {
                let meshInstanceId = new MeshInstanceId (node.GetId (), meshIndex);
                if (model.IsMeshInstanceVisible (meshInstanceId)) {
                    return true;
                }
            }
            for (let childNode of node.GetChildNodes ()) {
                if (NodeHasVisibleChildren (model, childNode)) {
                    return true;
                }
            }
            return false;
        }

        function AddNode (model, jsonParent, jsonNodes, node)
        {
            if (node.IsMeshNode ()) {
                for (let meshIndex of node.GetMeshIndices ()) {
                    AddMeshNode (model, jsonParent, jsonNodes, node, meshIndex, true);
                }
            } else if (NodeHasVisibleChildren (model, node)) {
                let nodeJson = {};

                let nodeName = node.GetName ();
                if (nodeName.length > 0) {
                    nodeJson.name = nodeName;
                }

                let transformation = node.GetTransformation ();
                if (!transformation.IsIdentity ()) {
                    nodeJson.matrix = node.GetTransformation ().GetMatrix ().Get ();
                }

                jsonNodes.push (nodeJson);
                jsonParent.push (jsonNodes.length - 1);

                nodeJson.children = [];
                AddChildNodes (model, nodeJson.children, jsonNodes, node);
            }
        }

        function AddMeshNode (model, jsonParent, jsonNodes, node, meshIndex, isStandaloneNode)
        {
            let meshInstanceId = new MeshInstanceId (node.GetId (), meshIndex);
            if (!model.IsMeshInstanceVisible (meshInstanceId)) {
                return;
            }

            let nodeJson = {
                mesh : model.MapMeshIndex (meshIndex)
            };
            if (isStandaloneNode) {
                let transformation = node.GetTransformation ();
                if (!transformation.IsIdentity ()) {
                    nodeJson.matrix = node.GetTransformation ().GetMatrix ().Get ();
                }
            }

            jsonNodes.push (nodeJson);
            jsonParent.push (jsonNodes.length - 1);
        }

        function AddChildNodes (model, jsonParent, jsonNodes, node)
        {
            for (let childNode of node.GetChildNodes ()) {
                AddNode (model, jsonParent, jsonNodes, childNode);
            }
            for (let meshIndex of node.GetMeshIndices ()) {
                AddMeshNode (model, jsonParent, jsonNodes, node, meshIndex, false);
            }
        }

        let mainJson = {
            asset : {
                generator : 'https://3dviewer.net',
                version : '2.0'
            },
            scene : 0,
            scenes : [
                {
                    nodes : []
                }
            ],
            nodes : [],
            materials : [],
            meshes : [],
            buffers : [],
            bufferViews : [],
            accessors : []
        };

        let rootNode = exporterModel.GetModel ().GetRootNode ();
        AddChildNodes (exporterModel, mainJson.scenes[0].nodes, mainJson.nodes, rootNode);

        for (let meshData of meshDataArr) {
            let jsonMesh = {
                name : this.GetExportedMeshName (meshData.name),
                primitives : []
            };

            let primitives = meshData.buffer.primitives;
            for (let primitiveIndex = 0; primitiveIndex < primitives.length; primitiveIndex++) {
                let primitive = primitives[primitiveIndex];

                let bufferViewCreator = new BufferViewCreator (mainJson, meshData.offsets[primitiveIndex]);
                let indicesBufferView = bufferViewCreator.AddBufferView (primitive.indices.length * this.components.index.size, GltfBufferType.ELEMENT_ARRAY_BUFFER);
                let verticesBufferView = bufferViewCreator.AddBufferView (primitive.vertices.length * this.components.number.size, GltfBufferType.ARRAY_BUFFER);
                let colorsBufferView = null;
                if (primitive.colors.length > 0) {
                    colorsBufferView = bufferViewCreator.AddBufferView (primitive.colors.length * this.components.number.size, GltfBufferType.ARRAY_BUFFER);
                }
                let normalsBufferView = bufferViewCreator.AddBufferView (primitive.normals.length * this.components.number.size, GltfBufferType.ARRAY_BUFFER);
                let uvsBufferView = null;
                if (primitive.uvs.length > 0) {
                    uvsBufferView = bufferViewCreator.AddBufferView (primitive.uvs.length * this.components.number.size, GltfBufferType.ARRAY_BUFFER);
                }

                let jsonPrimitive = {
                    attributes : {},
                    mode : 4,
                    material : primitive.material
                };

                let bounds = primitive.GetBounds ();

                mainJson.accessors.push ({
                    bufferView : indicesBufferView,
                    byteOffset : 0,
                    componentType : this.components.index.type,
                    count : primitive.indices.length,
                    type : 'SCALAR'
                });
                jsonPrimitive.indices = mainJson.accessors.length - 1;

                mainJson.accessors.push ({
                    bufferView : verticesBufferView,
                    byteOffset : 0,
                    componentType : this.components.number.type,
                    count : primitive.vertices.length / 3,
                    min : bounds.min,
                    max : bounds.max,
                    type : 'VEC3'
                });
                jsonPrimitive.attributes.POSITION = mainJson.accessors.length - 1;

                if (colorsBufferView !== null) {
                    mainJson.accessors.push ({
                        bufferView : colorsBufferView,
                        byteOffset : 0,
                        componentType : this.components.number.type,
                        count : primitive.colors.length / 3,
                        type : 'VEC3'
                    });
                    jsonPrimitive.attributes.COLOR_0 = mainJson.accessors.length - 1;
                }

                mainJson.accessors.push ({
                    bufferView : normalsBufferView,
                    byteOffset : 0,
                    componentType : this.components.number.type,
                    count : primitive.normals.length / 3,
                    type : 'VEC3'
                });
                jsonPrimitive.attributes.NORMAL = mainJson.accessors.length - 1;

                if (uvsBufferView !== null) {
                    mainJson.accessors.push ({
                        bufferView : uvsBufferView,
                        byteOffset : 0,
                        componentType : this.components.number.type,
                        count : primitive.uvs.length / 2,
                        type : 'VEC2'
                    });
                    jsonPrimitive.attributes.TEXCOORD_0 = mainJson.accessors.length - 1;
                }

                jsonMesh.primitives.push (jsonPrimitive);
            }

            mainJson.meshes.push (jsonMesh);
        }

        return mainJson;
    }

    ExportMaterials (exporterModel, mainJson, addTexture)
    {
        function ExportMaterial (obj, mainJson, material, addTexture)
        {
            function ColorToRGBA (color, opacity)
            {
                return [
                    SRGBToLinear (color.r / 255.0),
                    SRGBToLinear (color.g / 255.0),
                    SRGBToLinear (color.b / 255.0),
                    opacity
                ];
            }

            function ColorToRGB (color)
            {
                return [
                    SRGBToLinear (color.r / 255.0),
                    SRGBToLinear (color.g / 255.0),
                    SRGBToLinear (color.b / 255.0)
                ];
            }

            function GetTextureParams (mainJson, texture, addTexture)
            {
                if (texture === null || !texture.IsValid ()) {
                    return null;
                }

                if (mainJson.images === undefined) {
                    mainJson.images = [];
                }
                if (mainJson.textures === undefined) {
                    mainJson.textures = [];
                }

                let textureIndex = addTexture (texture);
                let textureParams = {
                    index : textureIndex
                };

                if (texture.HasTransformation ()) {
                    let extensionName = 'KHR_texture_transform';
                    if (mainJson.extensionsUsed === undefined) {
                        mainJson.extensionsUsed = [];
                    }
                    if (mainJson.extensionsUsed.indexOf (extensionName) === -1) {
                        mainJson.extensionsUsed.push (extensionName);
                    }
                    textureParams.extensions = {
                        KHR_texture_transform : {
                            offset : [texture.offset.x, -texture.offset.y],
                            scale : [texture.scale.x, texture.scale.y],
                            rotation : -texture.rotation
                        }
                    };
                }

                return textureParams;
            }

            let jsonMaterial = {
                name : obj.GetExportedMaterialName (material.name),
                pbrMetallicRoughness : {
                    baseColorFactor : ColorToRGBA (material.color, material.opacity)
                },
                emissiveFactor : ColorToRGB (material.emissive),
                doubleSided : true,
                alphaMode : 'OPAQUE'
            };

            if (material.transparent) {
                // TODO: mask, alphaCutoff?
                jsonMaterial.alphaMode = 'BLEND';
            }

            let baseColorTexture = GetTextureParams (mainJson, material.diffuseMap, addTexture);
            if (baseColorTexture !== null) {
                if (!material.multiplyDiffuseMap) {
                    jsonMaterial.pbrMetallicRoughness.baseColorFactor = ColorToRGBA (new RGBColor (255, 255, 255), material.opacity);
                }
                jsonMaterial.pbrMetallicRoughness.baseColorTexture = baseColorTexture;
            }
            if (material.type === MaterialType.Physical) {
                let metallicTexture = GetTextureParams (mainJson, material.metalnessMap, addTexture);
                if (metallicTexture !== null) {
                    jsonMaterial.pbrMetallicRoughness.metallicRoughnessTexture = metallicTexture;
                } else {
                    jsonMaterial.pbrMetallicRoughness.metallicFactor = material.metalness;
                    jsonMaterial.pbrMetallicRoughness.roughnessFactor = material.roughness;
                }
            }
            let normalTexture = GetTextureParams (mainJson, material.normalMap, addTexture);
            if (normalTexture !== null) {
                jsonMaterial.normalTexture = normalTexture;
            }
            let emissiveTexture = GetTextureParams (mainJson, material.emissiveMap, addTexture);
            if (emissiveTexture !== null) {
                jsonMaterial.emissiveTexture = emissiveTexture;
            }

            mainJson.materials.push (jsonMaterial);
        }

        for (let materialIndex = 0; materialIndex < exporterModel.MaterialCount (); materialIndex++) {
            let material = exporterModel.GetMaterial (materialIndex);
            ExportMaterial (this, mainJson, material, addTexture);
        }
    }
}
